﻿//#define CSVDEBUG

using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

namespace NETXPF.Library
{
    /// <summary>
    /// The easiest CSV interface you'll ever find.
    /// Provides methods for handling the CSV format properly (read AND write).
    /// CSV format is officially documented in RFC 4180: http://www.ietf.org/rfc/rfc4180.txt
    /// </summary>
    public static class CSVAdapter
    {
        private enum State
        {
            Incomplete,
            Quoted,
            Escaped
        }
        /// <summary>
        /// Reads entire contents of stream and returns all records as a list.
        /// This reads all records into memory. If you want a low-memory method, use ReadRecord while streaming through the file.
        /// Use this method as a quick shortcut for small files (below 50 MB).
        /// </summary>
        /// <param name="stream">Stream to read</param>
        /// <returns>List of all records</returns>
        public static List<string[]> Read(Stream stream)
        {
            List<string[]> records = new List<string[]>();
            using (StreamReader sr = new StreamReader(stream))
            {
                while (true)
                {
                    List<string> record = ReadRecord(sr);
                    if (record == null)
                        break;
                    records.Add(record.ToArray());
                }
            }
            return records;
        }
        /// <summary>
        /// Reads the next row (line) from CSV-formatted data. Returns null if end of file has been reached. Otherwise, it returns a list of column values for that row.
        /// </summary>
        /// <returns>A list of columns read from the row, or null if there is no next row</returns>
        public static List<string> ReadRecord(TextReader reader)
        {
            // Build the list of columns in the line
            List<string> columns = new List<string>();
            if (ExtractColumns(reader, columns))
            {
#if CSVDEBUG
                foreach (string col in columns)
                {
                    Console.Write(col);
                    Console.Write("||");
                }
                Console.WriteLine();
#endif
                return columns;
            }
            return null;
        }

        /// <summary>
        /// Produces a CSV-formatted record from an array of column values.
        /// </summary>
        /// <param name="columnValues"></param>
        /// <returns></returns>
        public static string ToCSV(string[] columnValues)
        {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < columnValues.Length; i++)
            {
                if (i > 0)
                {
                    sb.Append(",");
                }
                sb.Append("\"");
                sb.Append(columnValues[i].Replace("\"", "\"\""));
                sb.Append("\"");
            }
            return sb.ToString();
        }

        /// <summary>
        /// Writes an entire table (set of rows) to CSV.
        /// </summary>
        /// <param name="table"></param>
        /// <returns></returns>
        public static string ToCSV(List<string[]> table)
        {
            StringBuilder sb = new StringBuilder();
            foreach (string[] row in table)
            {
                sb.AppendLine(ToCSV(row));
            }
            return sb.ToString();
        }

        private static bool ExtractColumns(TextReader reader, List<string> columns)
        {
            StringBuilder accumulator = new StringBuilder();
            State currentState = State.Incomplete;
            //use a state machine approach
            if (reader.Peek() < 0)
            {
                return false; //end of file
            }
            while(reader.Peek() >= 0)
            {
                char c = (char)reader.Read();
                bool accumulate = true;
                if (c == '"')
                {
                    if (currentState == State.Incomplete)
                    {
                        currentState = State.Quoted;
                        accumulate = false;
                    }
                    else if (currentState == State.Quoted)
                    {
                        currentState = State.Escaped;
                        accumulate = false;
                    }
                    else if (currentState == State.Escaped)
                    {
                        currentState = State.Quoted;
                    }
                }
                if (c == ',' || c == '\r' || c == '\n')
                {
                    if (currentState == State.Incomplete || currentState == State.Escaped)
                    {
                        //accept (implicit state)
                        columns.Add(accumulator.ToString());
                        accumulator = new StringBuilder();
                        accumulate = false;
                        currentState = State.Incomplete;
                        if (c == '\r' || c == '\n')
                        {
                            if (c == '\r' && reader.Peek() == '\n')
                            {
                                reader.Read(); //ignore newline char (part of CRLF)
                            }
                            return true; //there might be more
                        }
                    }
                }
                if (accumulate)
                {
                    accumulator.Append(c);
                }
            }
            columns.Add(accumulator.ToString());
            return true; //columns exist, so ignore end of file this time around (we will catch it on next call)
        }
    }
}
